// Copyright Epic Games, Inc. All Rights Reserved.

#include "Engine/BlueprintGeneratedClass.h" // 包含蓝图生成类头文件
#include "UObject/UObjectIterator.h" // 包含UObject迭代器头文件
#include "HAL/FileManager.h" // 包含文件管理器头文件
#include "Misc/Paths.h" // 包含路径工具头文件
#include "Engine/World.h" // 包含世界头文件

#include "LyraLogChannels.h" // 包含Lyra日志频道头文件

//////////////////////////////////////////////////////////////////////////

#if ALLOW_DEBUG_FILES // 如果允许调试文件
#include "HAL/IConsoleManager.h" // 包含控制台管理器头文件

/**
 * 写入一个指定名称的集合文件，包含一个项目列表（返回集合文件的绝对路径）
 * 这是手动的，而不是依赖集合管理器，因此可以在运行时使用，而不依赖于开发人员模块
 * @param CollectionName 集合名称。
 * @param Items 要包含在集合中的项目列表。
 * @return 集合文件的绝对路径，如果失败则返回空字符串。
 */
FString WriteCollectionFile(const FString& CollectionName, const TArray<FString>& Items)
{
	// 如果在编辑器中，创建在 CST_Local 会使用的目录中，否则将其写入性能分析目录以供后续收集
	const FString OutputDir = WITH_EDITOR ? (FPaths::ProjectSavedDir() / TEXT("Collections")) : (FPaths::ProfilingDir() / TEXT("AssetSnapshots")); // 确定输出目录

	IFileManager::Get().MakeDirectory(*OutputDir, true); // 创建输出目录（包括必要的父目录）

	const FString LogFilename = OutputDir / (CollectionName + TEXT(".collection")); // 构建完整的日志文件名

	if (FArchive* OutputFile = IFileManager::Get().CreateDebugFileWriter(*LogFilename)) // 创建调试文件写入器
	{
		const FGuid CollectionGUID = FGuid::NewGuid(); // 生成新的GUID用于集合

		OutputFile->Logf(TEXT("FileVersion:2")); // 写入文件版本
		OutputFile->Logf(TEXT("Type:Static")); // 写入集合类型：静态
		OutputFile->Logf(TEXT("Guid:%s"), *CollectionGUID.ToString(EGuidFormats::DigitsWithHyphens)); // 写入集合GUID
		OutputFile->Logf(TEXT("")); // 写入空行

		for (const FString& Item : Items) // 遍历所有项目
		{
			OutputFile->Logf(TEXT("%s"), *Item); // 将每个项目写入文件
		}

		// 刷新、关闭并删除文件写入器。
		delete OutputFile; // 删除文件写入器，会触发 flush 和 close

		const FString AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*LogFilename); // 获取文件的绝对路径
		return AbsolutePath; // 返回绝对路径
	}

	return FString(); // 如果创建文件失败，返回空字符串
}

/**
 * 控制台命令：将当前对象列表输出到一个集合文件中。
 * 用法：Lyra.ObjListToCollection [可选后缀]
 * @param Params 命令参数（可选，用于为集合文件名添加后缀）。
 * @param World 当前世界对象。
 */
FAutoConsoleCommandWithWorldAndArgs GObjListToCollectionCmd(
	TEXT("Lyra.ObjListToCollection"), // 命令名称
	TEXT("Spits out a collection that contains the current object list"), // 命令帮助文本
	FConsoleCommandWithWorldAndArgsDelegate::CreateStatic( // 创建静态委托
		[](const TArray<FString>& Params, UWorld* World) // Lambda 函数，接受参数和世界对象
{
	// 获取已加载资源的列表
	TArray<FString> AssetPaths; // 存储资源路径的数组
	for (TObjectIterator<UObject> It; It; ++It) // 遍历所有UObject
	{
		UObject* Obj = *It; // 获取当前对象
		if (Obj->IsAsset()) // 如果对象是资源
		{
			AssetPaths.Add(Obj->GetPathName()); // 添加对象的路径名到资源路径列表
		}
		else if (UBlueprintGeneratedClass* Class = Cast<UBlueprintGeneratedClass>(Obj)) // 如果对象是蓝图生成的类
		{
			FString BlueprintName = Class->GetPathName(); // 获取类的路径名
			BlueprintName.RemoveFromEnd(TEXT("_C")); // 移除末尾的 "_C" 后缀（蓝图生成类的标准后缀）
			AssetPaths.Add(BlueprintName); // 添加处理后的蓝图名称到资源路径列表
		}
	}
	AssetPaths.Sort(); // 对资源路径列表进行排序

	// 确定文件名
	FString CollectionNameSuffix; // 集合名称后缀
	if (Params.Num() > 0) // 如果有参数
	{
		CollectionNameSuffix = TEXT("_") + Params[0]; // 使用第一个参数作为后缀
	}
	// 构建集合名称，包含时间戳和地图名称
	const FString CollectionName = FString::Printf(TEXT("_LoadedAssets_%s_%s%s"), *FDateTime::Now().ToString(TEXT("%H%M%S")), *GWorld->GetMapName(), *CollectionNameSuffix);

	// 写出集合文件
	const FString CollectionFilePath = WriteCollectionFile(CollectionName, AssetPaths); // 调用 WriteCollectionFile 函数写入文件
	UE_LOG(LogLyra, Warning, TEXT("Wrote collection of loaded assets to %s"), *CollectionFilePath); // 记录日志，显示集合文件路径
}));

#endif // ALLOW_DEBUG_FILES 结束

//////////////////////////////////////////////////////////////////////////

/**
 * 用于分析对象列表与父类（蓝图或C++默认值）的差异，以确定是否有任何字段实际上是“固定的”并且可以移除以节省内存。
 * 可以在命令中使用。
 * @param ObjectList 要分析的对象列表视图。
 * @param CommonClass 共同的父类，用于比较。
 * @param PropertiesToIgnore 要忽略的属性名称集合。
 * @param bLogAllMatchedDefault 是否记录所有匹配默认值的属性（用于详细日志）。
 */
void AnalyzeObjectListForDifferences(TArrayView<UObject*> ObjectList, UClass* CommonClass, const TSet<FName>& PropertiesToIgnore, bool bLogAllMatchedDefault=false)
{
	check(CommonClass); // 确保 CommonClass 有效
	UObject* CommonClassCDO = CommonClass->GetDefaultObject(); // 获取公共类的默认对象（CDO）

	UE_LOG(LogLyra, Log, TEXT("  Field\tDifferentToBase\tNumValues\tValues")); // 输出表头日志

	for (TFieldIterator<FProperty> PropIt(CommonClass); PropIt; ++PropIt) // 遍历公共类的所有属性
	{
		FProperty* Prop = *PropIt; // 获取当前属性
		if (PropertiesToIgnore.Contains(Prop->GetFName())) // 如果属性在忽略列表中
		{
			continue; // 跳过此属性
		}

		//@TODO: 处理固定长度的数组
		ensure(Prop->ArrayDim <= 1); // 确保属性数组维度不超过1（目前不支持固定长度数组）

		FString DefaultValueStr; // 存储默认值的字符串表示
		Prop->ExportText_InContainer(0, /*out*/ DefaultValueStr, CommonClassCDO, CommonClassCDO, nullptr, 0); // 从CDO导出属性的文本表示

		bool bAnyMatchedDefaultValue = false; // 标记是否有任何对象匹配默认值
		bool bAllMatchedDefaultValue = true; // 标记是否所有对象都匹配默认值

		TSet<FString> ValuesObserved; // 存储观察到的不同值的集合
		for (UObject* Object : ObjectList) // 遍历对象列表中的每个对象
		{
			FString ValueStr; // 存储当前对象属性值的字符串表示
			if (Prop->ExportText_InContainer(0, /*out*/ ValueStr, Object, CommonClassCDO, nullptr, 0)) // 从当前对象导出属性的文本表示
			{
				ValuesObserved.Add(ValueStr); // 将值添加到观察到的值集合中
				bAllMatchedDefaultValue = false; // 发现有对象的值与默认值不同，标记为 false
			}
			else // 如果导出失败（例如，属性不可读）
			{
				bAnyMatchedDefaultValue = true; // 标记为有对象匹配默认值（这里可能表示使用了默认值？逻辑需审视）
			}
		}

		if (bAnyMatchedDefaultValue) // 如果有任何对象匹配默认值（或导出失败）
		{
			ValuesObserved.Add(DefaultValueStr); // 将默认值也添加到观察到的值集合中
		}

		if (!bAllMatchedDefaultValue) // 如果不是所有对象都匹配默认值（即存在差异）
		{
			const FString ValueList = FString::Join(ValuesObserved, TEXT(",")); // 将观察到的值集合连接成逗号分隔的字符串

			// 记录差异信息：类名、属性名、差异类型（固定不同或变化）、观察到的值数量、值列表
			UE_LOG(LogLyra, Log, TEXT("  %s::%s\t%s\t%d\t%s"),
				*CommonClass->GetName(), // 类名
				*Prop->GetName(), // 属性名
				(ValuesObserved.Num() == 1) ? TEXT("FixedDifferent") : TEXT("Varies"), // 如果只有一个值，则是“固定不同”，否则是“变化”
				ValuesObserved.Num(), // 观察到的不同值的数量
				*ValueList); // 观察到的值列表
		}
		else if (bLogAllMatchedDefault) // 如果所有对象都匹配默认值，并且要求记录所有匹配默认值的属性
		{
			UE_LOG(LogLyra, Log, TEXT("  %s::%s\t%s"), *CommonClass->GetName(), *Prop->GetName(), TEXT("Default")); // 记录属性使用默认值
		}
	}
}

//////////////////////////////////////////////////////////////////////////